/* 
 * File:   raytracer.cpp
 * Author: chaterolas
 * 
 * Created on 24 de junio de 2009, 10:42 PM
 */

#include "raytracer.h"

/**
 * Crea una nueva instancia de Ray Tracer a partir de los siguientes parametros:
 * @param scn (escena a trazar)
 * @param depth
 */
RayTracer::RayTracer(Scene* scn, int depth) {
    w = scn->getWidth();
    h = scn->getHeight();
    s = scn->getAAPasses();
    scene = scn;
    fb = new Color *[w*h];
    this->depth = depth;

    for(int i = 0; i < h; i++)
        for(int j = 0; j < w; j++)
            fb[p(j,i)] = new Color();
}

/**
 * Destruye una instancia deray tracer junto con sus componentes pertinentes
 */
RayTracer::~RayTracer() {
    delete scene;
    for(int i = 0; i < h; i++)
        for(int j = 0; j < w; j++)
            delete fb[p(j,i)];

    delete [] fb;
    delete ulb;
    delete urb;
    delete blb;
    delete brb;
    delete ax;
    delete ay;
}

/**
 * Return hte location in the frame buffer for the coordinate (x,y,z), using a
 * directionament polinomial.
 * @return location respective to (x,y,z)
 *
 */
int RayTracer::p(int x, int y) {
    return x + y*w;
}

/**
 * Regresa un numero de punto flotante entre 0 y 1. distribuido uniformemente.
 * @return A double ramdom number between 0 to 1
 */
double RayTracer::randomNumber() {
    double value = (double) rand()/RAND_MAX;
    if(value>1)
         value = 0;

    return value;
}

/**
 * Regresa el primitivo mas cercano con el cual se intersecta un rayo dado.
 * Si el rayo no intersecta nada entonces se regresa una referencia nula y
 * una distancia mayor al limite permitido para trazar.
 * @param ray
 * @return first primitive that the ray intersect
 */
Primitive *RayTracer::nearest(Ray *r) {
    double nearest = DIST_LIMT, dpar = 0.0;
    int index = 0;

    for(int i = 0; i < scene->primitivesCount(); i++) {
        dpar = scene->getPrimitives()[i]->intersect(r);

        if(dpar < nearest) {
            nearest = dpar;
            index = i;
        }
    }

    if(nearest < DIST_LIMT) {
        d = nearest;        
        return scene->getPrimitives()[index];
    } else {
        return NULL;
    }
}

/**
 * Regresa un vector que representa la direccion en que se encuentra la entidad
 * luminosa partir de la cual se esta iluminado el objeto a trazar.
 * @param i (punto de interseccion de un rayo dado con un primitivo)
 * @param l (fuente luminosa que se quiere referir)
 * @return vector (que va de la interseccion del rayo con el primitivo
 * a la posicion donde se esncuetra la fuente luminosa)
 */
Vector3D *RayTracer::getLightDir(Vector3D* i, Light* l) {
    AreaLight *area;
    Vector3D *temp, *t1, *t2, *t3;

    switch(l->getType()) {
        case POINT:
            temp = *l->getPosition() - *i;
            temp->normalize();
            return temp;
        case SURFACE:
            area = (AreaLight *) l;
            t1 = *area->getCorner() * 0.5;
            t2 = *area->getPosition() + *t1;
            t3 = *t2 - *i;
            t3->normalize();

            delete t1;
            delete t2;

            return t3;
        default : // unsopported light primitive
            return NULL;
    }
}

/**
 * Regresa el color asociado a el color especular que se proyecta en el
 * primitivo en el primitivo, partir de la direccion del vector que va de la
 * interseccion del primitivo a el punto de luz, el punto de interseccion el
 * rayo que impacto y el primitivo con el cual lo hizo.
 * @param ld (direccion de la luz)
 * @param in (direccion de la interseccion)
 * @param r (rayo que que impacto el primitivo)
 * @param p (primitivo con el cual impacto el rayo)
 * @return color (el color generado por el efecto especular de la luz)
 */
Color *RayTracer::getLightSpecular(Vector3D* ld, Vector3D* in, Ray* r, Primitive* p) {
    Vector3D *n = p->getNormal(*in);
    Vector3D *temp = (*n) * (2.0 * ld->dot(*n));
    Vector3D *rv = *ld - *temp;
    double specular = rv->dot(*r->getDirection());
    double spec = 0.0;

    if(specular > 0)
        spec = pow(specular, p->getMaterial()->spechard) * (p->getMaterial()->specular) * (n->dot(*ld));

    Color *slc = new Color(1.0,1.0,1.0);
    slc->scale(spec);

    delete n;
    delete rv;
    delete temp;

    return slc;
}


/**
 * Regresa el primitivo mas cercano cuya sombra se proyecta sobre la interseccion
 * con el primitivo actual.
 * @param r (rayo rebotado)
 * @param l (fuente de luz desde donde se hace referencia)
 * @return primitive
 */
Primitive *RayTracer::getshadowInt(Ray *r, Light *l) {
    Vector3D *temp = *l->getPosition() - *r->getOrigin();
    double dist = temp->getNorm();
    double nearest = dist + 1;
    int index = 0;

    delete temp;

    for(int i = 0; i < scene->primitivesCount(); i++) {
        double d = scene->getPrimitives()[i]->intersect(r);

        if(d < nearest && d > EPSILON) {
            nearest = d;
            index = i;
        }
    }

    if(nearest >= dist) {
        return NULL;
    } else {
        d = nearest;
        return scene->getPrimitives()[index];
    }
}

/**
 * Regresa la intensidad de la sombra en el punto de interseccion de algun
 * primitivo con un rayo, con respecto a la luz una luz en la escena.
 * @param l (Luz de referencia)
 * @param ld (direccion en la que esta la luz referida)
 * @param in (punto de interseccion del rayo con el primitivo)
 * @return shade (intensidad de la sombra segun su oclusion con respecto a otros
 * objetos)
 */
double RayTracer::calcShade(Light *l, Vector3D *ld, Vector3D *in) {
    double retval;
   
    if(l->getType() == SURFACE) {

        int lsc = l->getSampleCount();
        retval = 0.0;
        AreaLight *area = (AreaLight *) l;

        for (int x = 0; x < lsc; x++) {
            for (int y = 0; y < lsc; y++) {
                Vector3D *left, *ad, *dir;
                Vector3D *t1, *t2, *t3, *t4;

                t1 = *area->getVSide() * ((double) (randomNumber() + x)*(1.0/lsc) + EPSILON);
                left = *area->getPosition() + *t1;
                delete t1;
                   
                t2 = *area->getHSide() * ((double) (randomNumber() + y)*(1.0/lsc) + EPSILON);
                ad = *left + *t2;
                delete t2;
                delete left;
                    
                dir = *ad - *in;
                dir->normalize();
                delete ad;

                t4 = *dir * EPSILON;

                t3 = *in + *t4;
                delete t4;

                Ray *lr = new Ray(*t3, *dir);
                delete t3;
                delete dir;

                Primitive *si = getshadowInt(lr, l);
                delete lr;

                if (si == NULL)
                    retval += 1.0 / (lsc*lsc);
            }
        }

            return retval;
    } else if(l->getType() == POINT) {
            Ray *lr1 = new Ray(*in, *ld);
            Primitive *si1 = getshadowInt(lr1, l);
            retval = (si1 == NULL) ? 1.0 : 0.0;

            delete lr1;

            return retval;
    }  else {
        // Unsupoorted type for primitive light
    }            
}

/**
 * Regresa el color reflejado por otros primitivos en la superficie del
 * primitivo que es actualmente intersectado.
 * @param p (primitivo al que se le calcula la reflexion)
 * @param in (punto d einterseccion del rayo con el primitivo)
 * @param r (rayo actual)
 * @param depth (profundidad de recursion en la que va el algoritmo)
 * @return color (el asociado a la reflexion en el punto de interseccion del
 * rayo con el primitivo)
 */

Color *RayTracer::getReflectColor(Primitive *p, Vector3D *in, Ray *r, int depth)
{
    Color *rc = new Color();
    Vector3D *n = p->getNormal(*in);

    double rs = 2.0 * n->dot(*r->getDirection());

    Vector3D *t1 = *n * rs;
    delete n;

    Vector3D *dr = *r->getDirection() - *t1;
    dr->normalize();
    delete t1;
    
    Vector3D *t2 = *dr * EPSILON;
    
    Vector3D *pos = *in + *t2;
    delete t2;

    Ray *rr = new Ray(*pos, *dr);
    delete pos;
    delete dr;

    rayTrace(rr, rc, depth+1);
    delete rr;

    rc->scale(p->getMaterial()->reflect);

    return rc;
}

/**
 * Regresa el color inducido por el indice de refraccion del material con el que
 * esta hecho el primitivo.
 * @param p (Primitivo a referencia para calcular la refraccion)
 * @param in (Punto de interseccion del primitivo con el rayo)
 * @param r (Actual rayo lanzado)
 * @param depth (Punto actual de la recursion)
 * @return color (El color generado por el fenomeno de refraccion)
 */
Color *RayTracer::getRefractionColor(Primitive *p, Vector3D *in, Ray *r, int depth)
{
    Vector3D *nor = p->getNormal(*in);

    double ior = p->getMaterial()->ior;
    double rc;

    if(nor->dot(*r->getDirection()) < 0) {
        rc = (double) 1.0 / ior;
    } else {
        rc = ior;
        *nor *= -1.0;
    }

    double c1 = r->getDirection()->dot(*nor) * -1.0;
    double c2 = 1.0 - rc*rc*(1 - c1*c1);
    c2 = sqrt(c2);

    Vector3D *t1 = *nor * c1;
    Vector3D *t2 = *nor * c2;
    delete nor;

    Vector3D *t3 = *r->getDirection() + *t1;
    delete t1;
    *t3 *= rc;
    *t3 -= *t2;
    delete t2;
    t3->normalize();

    Vector3D *t4 = *t3 * EPSILON;
    Vector3D *t5 = *in + *t4;
    delete t4;

    Ray *rref = new Ray(*t5, *t3);
    delete t3;
    delete t5;

    Color *color = new Color();
    rayTrace(rref, color, depth+1);
    delete rref;
        
    return color;
}

/**
 * Realiza el algoritmo de ray tracing para un rayo especifico, y recibe como
 * argumento el color del pixela colorear asi como la profundidad maxima a trazar.
 * @param r (rayo lanzado a través del pixel)
 * @param c (color asociado al pixel,e ste se modificara dentro del metodo)
 * @param depth (profundidad maxima de trazado, por le rayo)
 */
void RayTracer::rayTrace(Ray *r, Color *c, int depth) {
    if(depth > this->depth) {

    } else {
        // nearest primitives to ray r
        Primitive *p = nearest(r);
        Vector3D *temp = *r->getDirection() * d;

        // coordinate of the intersection of nearest primitive to ray r
        Vector3D *in = *r->getOrigin() + *temp;

        if(p != NULL) {
            Primitive **primitives = scene->getPrimitives();

            // normal vector throug in to neasrest primitive
            Vector3D *ln = p->getNormal(*in);

            for(int j = 0; j < scene->primitivesCount(); j++) {

                if(primitives[j]->isLight()) {
                    Light *lite = (Light *) primitives[j];

                    // direction of in to lite
                    Vector3D *ld = getLightDir(in, lite);

                    // calcula la intensidad de la sombra en el punto de interseccion
                    double shade = calcShade(lite, ld, in);

                    if(shade > 0.0) {
                        double coediff =ld->dot(*ln) * p->getMaterial()->diffuse
                        * lite->getIntesity() * shade;

                        if(coediff > EPSILON) {
                            Color *dlc = new Color(*p->getMaterial()->color);
                            dlc->blend(*lite->getColor());
                            dlc->scale(coediff);
                            c->add(*dlc);

                            delete dlc;
                        }

                        // specular is not used yet
                        Color *specular = getLightSpecular(ld, in, r, p);
                        specular->scale(shade);
                        c->add(*specular);

                        delete specular;
                    }

                    delete ld;
                }
            }
            
            delete ln;

            // Add the reflection effect to the color
            if(p->getMaterial()->reflect > EPSILON){
                Color *rc = getReflectColor(p, in, r, depth);
                Color *rc2 = new Color(*rc);
                
                rc2->scale(0.5);
                rc->blend(*p->getMaterial()->color);

                rc->scale(0.5f);

                c->add(*rc);
                delete rc;

                c->add(*rc2);
                delete rc2;
            }

            // Add the refraction effect to the color
            if(p->getMaterial()->refract > EPSILON){
                Color *rc = getRefractionColor(p, in, r, depth);
                rc->scale(p->getMaterial()->refract);
                c->add(*rc);
                delete rc;
            }
        }

        delete temp;
        delete in;
    }
}

/**
 * Coloca en posicion correcta la camara apartir de so posicion y orientacion de
 * vision
 */
void RayTracer::setCameraPosition() {
    Camera *cam = scene->getCamera();

    int a = scene->getWidth(); int b = scene->getHeight();
     
    while(b != 0){
        int t = b;
        b = a % b;
        a = t;
    }

    pL = (double) scene->getWidth() / a;
    pT = (double) scene->getHeight() / a;
    double pR = -1.0*pL;
    double pB = -1.0*pT;

    /* Now create an XY coplanar rectangle for the focal plane */
    Vector3D *t1 = new Vector3D(pL, pT, 0.0);
    Vector3D *t2 = new Vector3D(pR, pT, 0.0);
    Vector3D *t3 = new Vector3D(pL, pB, 0.0);
    Vector3D *t4 = new Vector3D(pR, pB, 0.0);

    /* Build an orthonormal base with the camera direction */
    Vector3D *up = new Vector3D(0,1,0);

    Vector3D *cz = new Vector3D(*cam->getDirection());
    Vector3D *cx = up->cross(*cz);
    Vector3D *cy = cz->cross(*cx);

    cz->normalize();
    cx->normalize();
    cy->normalize();

    /* Compute the focal plane rotation matrix */
    Matriz *tm = new Matriz(*cx, *cy, *cz);    
    tm->transpose();
    delete cx;
    delete cy;
    delete cz;

    /* Apply the matrix */
    Vector3D *t5 = *tm * (*t1);
    delete t1;

    Vector3D *t6 = *tm * (*t2);
    delete t2;
        
    Vector3D *t7 = *tm * (*t3);
    delete t3;

    Vector3D *t8 = *tm * (*t4);
    delete t4;

    /* Translate the camera to the specified origin */
    ulb = *t5 + *cam->getOrigin();
    delete t5;

    urb = *t6 + *cam->getOrigin();
    delete t6;

    blb = *t7  + *cam->getOrigin();
    delete t7;

    brb = *t8  + *cam->getOrigin();
    delete t8;

    /* Move the camera origin by the number of units of the
     * observer from the focal plane */
    Vector3D *nO1 = new Vector3D(0,0, -1.0*F_DIST);
    Vector3D *nO2 =  *tm * *nO1;
    delete nO1;    

    *nO2 += *cam->getOrigin();    
    
    cam->setOrigin(*nO2);
    delete nO2;


    /* Compute de sides of the focal plane used to interpolate
     * when rendering */
    ax = *blb - *brb;
    ay = *urb - *brb;   
}



/**
 * Escribe la imagne generada por el trazado con rayos realizado, apartir del
 * nombre de la escena.
 */
void RayTracer::outputImage() {

  //int s = sqrtSamplesPerPixel*sqrtSamplesPerPixel;
  int pac = 0;
  string file = scene->getName();
  pngwriter image_out(w, h, 0, file.append(".png").c_str());

  for(register int y = 1; y <= h; y++)
    for(register int x = 1; x <= w; x++) {
      pac = p(x - 1, y - 1);
      image_out.plot(x, y, fb[pac]->getChanel(0), fb[pac]->getChanel(1),
              fb[pac]->getChanel(2));
    }

  image_out.close();
  image_out.clear();

  cout << "outputimage writed \n";
}


/**
 * Metodo que renderea la imagen a prosesar, recibe el tamaño de vision del
 * plano focal. SUpondremos que el plano focal es un subconjunto del plano x,y,
 * & que la camara se encuentra por abajo de este plano, es decir su coordenada
 * en z es negativa
 */
void RayTracer::render() {   

    register int pac, k, s1, s2, ver, hor;
    double sco = (double) 1.0 / (s*s);

    setCameraPosition();

    for(ver = 0; ver < h; ver++) {
        for(hor = 0; hor < w; hor++) {

            pac = p(hor, ver);

            for(s1 = 0; s1 < s; s1++) {
                for(s2 = 0; s2 < s; s2++) {

                    Vector3D *temp1 = *ay * (ver*1.0/(h-1) + s1*1.0/((h-1)*s) + EPSILON);
                    Vector3D *left = *brb + *temp1;
                    delete temp1;

                    Vector3D *temp2 = *ax * (hor*1.0/(w-1) + s2*1.0/((w-1)*s) + EPSILON);
                    Vector3D *ad1 = *left + *temp2;
                    delete temp2;

                    Vector3D *ad = *ad1 - *scene->getCamera()->getOrigin();
                    ad->normalize();
                    delete ad1;

                    Ray *r = new Ray(*scene->getCamera()->getOrigin(), *ad);
                    delete ad;

                    Color *c = new Color();

                    rayTrace(r, c, 0);
                    delete r;

                    for(k = 0; k < 3; k++) {
                        if(c->getChanel(k) > 1)
                            c->setChanel(k, 1.0);

                        c->scale(sco);

                        fb[pac]->add(*c);
                    }
                    delete c;
                }
            }
        }
    }

    outputImage();

    cout << "render termina \n";
}